package it.javapersonalutility.xmlwrapper;

import java.io.InputStream;
import java.io.StringWriter;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

/**
 * Wrapper class to handle xml document
 * 
 * @author Sergio Davini
 *
 */
public class XMLWrapper {
	
	/**
	 * wrapped xml document
	 */
	private Document document = null;
	

	/**
	 * constructor from xml Document
	 * @param doc
	 */
	public XMLWrapper(Document doc) {
		this.document = doc;
	}
	
	/**
	 * constructor from an Element
	 * @param doc
	 */
	public XMLWrapper(Element source) throws Exception {
		document = getBuilder().newDocument();
		Element e = createElement(source);
		document.appendChild(e);
		cloneElement(e, source);
		
	}
	
	/**
	 * constructor from InputStream of xml document
	 * @param is
	 * @throws Exception
	 */
	public XMLWrapper(InputStream is) throws Exception  {
		document = getBuilder().parse(is);
		is.close();
	}	


	/**
	 * return the root element of the wrapped document
	 * @return
	 */
	public Element getRootElement() {
		return this.document.getDocumentElement();
	}

	public Document getDocument() {
		return this.document;
	}
	

	/**
	 * add another XMlWrapper document to this document.
	 * 
	 *  the second document is added as child of the first document's root
	 * @param d
	 */
	public void addDoc(XMLWrapper d) {
		addDoc(d.document);
	}

	public void addDoc(Document doc) {
		
		Element source = doc.getDocumentElement();
		
		// verifico se aggiungo un documento ad un XMLWrapper vuoto
		if (document.getDocumentElement()!=null) {
			addElement(document.getDocumentElement(), source);
		} else {
			Element e = createElement(source);
			document.appendChild(e);
			cloneElement(e, source);
		}
		
	}

	private Element createElement(Element e) {
		return document.createElement(e.getTagName());
	}
	
	/**
	 * execute an xPath returning a String value
	 * @param stringPath
	 * @return
	 * @throws Exception
	 */
	public String extractStringValue(String stringPath) throws Exception {
		return (String)extractValue(stringPath, XPathConstants.STRING);
	}

	/**
	 * execute an xPath returning a boolean value
	 * @param stringPath
	 * @return
	 * @throws Exception
	 */
	public boolean extractBooleanValue(String stringPath) throws Exception {
		return (Boolean)extractValue(stringPath, XPathConstants.BOOLEAN);
	}


	/**
	 * execute an xpath returning a numer value
	 * @param stringPath
	 * @return
	 * @throws Exception
	 */
	public Double extractNodeNumber(String stringPath) throws Exception {
		return (Double)extractValue(stringPath, XPathConstants.NUMBER);
	}


	/**
	 * execute an xpath returning a list of nodes
	 * @param stringPath
	 * @return
	 * @throws Exception
	 */
	public NodeList extractNodeValue(String stringPath) throws Exception {
		return (NodeList)extractValue(stringPath, XPathConstants.NODESET);
	}
	
	/**
	 * executer an xpath returning a xmlWrapper.
	 * the xpath must indicate an element. 
	 * @param stringpath
	 * @return
	 * @throws Exception
	 */
	public XMLWrapper extraxtXmlWrapper(String stringpath) throws Exception {
		NodeList list = extractNodeValue(stringpath);
		if (list.getLength()>0) {
			Node node = list.item(0);
			if (node instanceof Element) {
				Element e = (Element)node;
				return new XMLWrapper(e);
			}
		}
		return null;
	}

	private Object extractValue(String stringPath, QName qname) throws Exception {
		XPath xpath = XPathFactory.newInstance().newXPath();
		XPathExpression expression = xpath.compile(stringPath);
		Object result = expression.evaluate(document, qname);
		return result;
	}

	/*

	*/

	/*
	public Element setValue(String name, String value) {
		Element root = document.getDocumentElement();
		Element figlio = getFiglio(root, name);
		figlio.setTextContent(value);
		return figlio;
	}
	
	public Element addValue(String name, String value) {
		Element root = document.getDocumentElement();
		Element figlio = document.createElement(name);
		figlio.setTextContent(value);
		root.appendChild(figlio);
		return figlio;
	}
	
	public Element getValue(String name) {
		Element root = document.getDocumentElement();
		Element figlio = getFiglio(root, name);
		return figlio;
	}


	
	public Element getFiglio(Element root, String tagName) {
		Element figlio = null;
		
		NodeList nodelist = root.getChildNodes();
		for (int i = 0; i < nodelist.getLength(); i++) {
			Node node = nodelist.item(i);
			if (node instanceof Element) {
				Element x = (Element) node;
				if (x.getNodeName().equals(tagName)) {
					figlio = (Element)node;
					break;
				}
			}
			
		}
		
		return figlio;
	}

	public String getFiglioContent(Element root, String tagName) {
		Element figlio = getFiglio(root, tagName);
		if (figlio!=null) {
			return figlio.getTextContent();
		} else {
			return null;
		}
	}
	
	public void addDoc(Element e) {

		addElement(document.getDocumentElement(), e);
		
	}
	*/

	/**
	 * add the elementSource, and all the subtree, as child of the elementDest
	 * @param elementDest
	 * @param elementSource
	 */
	private void addElement(Element elementDest, Element elementSource) {
		
		// create the child element
		Element elementNew = document.createElement(elementSource.getTagName());
		elementDest.appendChild(elementNew);

		cloneElement(elementNew, elementSource);
	}

	private void cloneElement(Element elementDest, Element elementSource) {
		
		// set all the attribute
		NamedNodeMap attrs = elementSource.getAttributes();
		for (int i = 0; i<attrs.getLength(); i++) {
			Node n = attrs.item(i);
			elementDest.setAttribute(n.getNodeName(), n.getNodeValue());
		}
		
		// add all the childnodes
		NodeList lista = elementSource.getChildNodes();
		for (int i = 0; i<lista.getLength(); i++) {
			Node nodo = lista.item(i);
			if (nodo instanceof Element) {
				addElement(elementDest, (Element)nodo);
			} else if (nodo instanceof Text) {
				Text testo = document.createTextNode(((Text)nodo).getNodeValue());
				elementDest.appendChild(testo);
			}
		}
		
	}

	/*
	public Element addPathNodo(String nome) {
		Element element = document.getDocumentElement();
		return addPathNodo(element, nome);
	}

	public Element addPathNodo(Element element, String nome) {
		
		LinkedList<String> path = parseNome(nome);
		while (path.size()>0) {
			String nodo = path.removeFirst();
			boolean last = (path.size()==0);
			element = getOrCreate(element, nodo, last);
		}
		
		return element;
	}

	public Element addPathNodo(String nome, String value) {
		Element element = addPathNodo(nome);
		element.setTextContent(value);
		return element;
	}

	public Element addPathCdata(String nome, String value) {
		Element element = addPathNodo(nome);
		CDATASection s = element.getOwnerDocument().createCDATASection(value);
		element.appendChild(s);
		return element;
	}

	public Element addPathCdata(Element padre, String nome, String value) {
		Element element = padre.getOwnerDocument().createElement(nome);
		padre.appendChild(element);
		CDATASection s = element.getOwnerDocument().createCDATASection(value);
		element.appendChild(s);
		return element;
	}

	private Element getOrCreate(Element padre, String nodo, boolean last) {
		Element element = null;
	
		
		if (padre==null) { 
			element = document.createElement(nodo);
			document.appendChild(element);
		} else {
			if (!last) {
				element = getElementByName(padre, nodo);
			}
			if (element==null) {
				element = document.createElement(nodo);
				padre.appendChild(element);
			}
		}
		
		return element;
	}

	private LinkedList<String> parseNome(String nome) {
		LinkedList<String> path = new LinkedList<String>();
		
		StringTokenizer st = new StringTokenizer(nome, ".");
		while (st.hasMoreTokens()) {
			path.add(st.nextToken());
		}
		
		return path;
	}
	
	private Element getElementByName(Element padre, String nome) {
		
		Element figlio = null;
		
		NodeList lista = padre.getChildNodes();
		for (int i = 0; i < lista.getLength(); i++) {
			Node node = lista.item(i);
			if (node instanceof Element) {
				Element x = (Element)node;
				if (nome.equals(x.getNodeName())) {
					figlio = x;
					break;
				}
			}
		}
		
		return figlio;
	}
	*/
	
	private DocumentBuilder getBuilder() throws Exception {
		DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
		domFactory.setNamespaceAware(true); 
		return domFactory.newDocumentBuilder();
	}
	
	public String toString() {
		try {
			DOMSource source = new DOMSource(document);
			StringWriter stringWriter = new StringWriter();
			StreamResult result = new StreamResult(stringWriter);
			Transformer transformer = TransformerFactory.newInstance().newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			transformer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
			transformer.transform(source, result);
			return stringWriter.getBuffer().toString();	
		} catch(Exception e) {
			e.printStackTrace();
			return "xml toString error";
		}
	}
	
	

}
